﻿namespace My.Hydrator
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Reflection;
    using My.Hydrator.Configuration;
    using My.Hydrator.Conventions;
    using My.Hydrator.Formatters;
    using My.Hydrator.Helpers;

    public class Hydrator
    {
        public Hydrator()
        {
            this.Container = new Container();
        }

        private Container Container { get; set; }

        private IEnumerable<ITypeConvention> Conventions
        {
            get
            {
                return this.Container.Conventions;
            }
        }

        private IEnumerable<KeyValuePair<string, IDictionary<string, ITypeConvention>>> CustomConventions
        {
            get
            {
                return this.Container.CustomConventions;
            }
        }

        private IEnumerable<IDataTypeFormatter> Formatters
        {
            get
            {
                return this.Container.Formatters;
            }
        }

        public T Hydrate<T>() where T : new()
        {
            return Hydrate(() => new T());
        }

        public T Hydrate<T>(Func<T> creator)
        {
            var type = typeof(T);
            if (!type.IsClass)
            {
                throw new ArgumentException();
            }

            if (creator == null)
            {
                throw new ArgumentException();
            }

            var convention = this.GetConvention(type);
            if (convention != null)
            {
                return (T)convention.GenerateValue(type.GetCustomAttributes(true));
            }

            var result = creator();

            this.HydrateProperties(result, type.GetProperties(), string.Empty, type, type);

            return result;
        }

        public void Configure(Action<ConfigurationExpression> configure)
        {
            lock (this)
            {
                var configurationExpression = new ConfigurationExpression();
                configure(configurationExpression);
            }
        }

        private ITypeConvention GetConvention(Type type)
        {
            return this.Conventions.SingleOrDefault(x => x.Type == type);
        }

        private ITypeConvention GetCustomConvention(Type baseType, Type previousPropertyType, string propertyNameSufix, PropertyInfo propertyInfo)
        {
            ITypeConvention result = null;

            var classConventions = this.CustomConventions.SingleOrDefault(x => x.Key == baseType.FullName).Value;
            if (classConventions != null)
            {
                string propertyName;
                if (!string.IsNullOrEmpty(propertyNameSufix))
                {
                    propertyName = string.Format("{0}.{1}", propertyNameSufix, propertyInfo.Name);
                }
                else
                {
                    propertyName = propertyInfo.Name;
                }

                result = classConventions.SingleOrDefault(x => x.Key == propertyName).Value;
            }

            if (result == null)
            {
                var propertyConventions = this.CustomConventions.SingleOrDefault(x => x.Key == previousPropertyType.FullName).Value;
                if (propertyConventions != null)
                {
                    result = propertyConventions.SingleOrDefault(x => x.Key == propertyInfo.Name).Value;
                }
            }

            return result;
        }

        private IDataTypeFormatter GetFormatter(DataType dataType)
        {
            return this.Formatters.SingleOrDefault(x => x.FormatterDataType.DataType == dataType);
        }

        private IDataTypeFormatter GetFormatter(string customDataType)
        {
            return this.Formatters.SingleOrDefault(x => x.FormatterDataType.CustomDataType == customDataType);
        }

        private void HydrateProperties(object objectToHydrate, IEnumerable<PropertyInfo> properties, string propertyNamePrefix, Type baseType, Type previousType)
        {
            foreach (var property in properties.Where(x => x.CanWrite))
            {
                var propertyName = property.Name;
                var propertyType = property.PropertyType;

                if (propertyType == baseType || propertyType == previousType)
                {
                    continue;
                }

                if (propertyType.IsNullable())
                {
                    var genericArguments = propertyType.GetGenericArguments();
                    var nullableType = genericArguments.FirstOrDefault();
                    if (nullableType != null)
                    {
                        var propertyInstance = new object();
                        if (propertyType.HasParameterlessConstructor())
                        {
                            propertyInstance = Activator.CreateInstance(propertyType);
                            property.SetValue(objectToHydrate, propertyInstance, null);
                        }

                        var convention = this.GetCustomConvention(baseType, previousType, propertyNamePrefix, property) ?? this.GetConvention(nullableType);

                        if (convention != null)
                        {
                            property.SetValue(objectToHydrate, convention.GenerateValue(property.GetCustomAttributes(true)), null);
                        }
                        else
                        {
                            this.HydrateProperties(propertyInstance, property.PropertyType.GetProperties().Where(x => x.CanWrite), propertyName, baseType, propertyType);
                        }
                    }
                }
                else if (propertyType.IsGenericType)
                {
                    if (propertyType.IsEnumerable())
                    {
                        var genericArguments = propertyType.GetGenericArguments();

                        if (propertyType.IsDictionary())
                        {
                            var genericKeyType = genericArguments[0];
                            var genericValueType = genericArguments[1];
                            var dictionaryType = typeof(Dictionary<,>).MakeGenericType(genericKeyType, genericValueType);

                            var convention = this.GetCustomConvention(baseType, previousType, propertyNamePrefix, property) ?? this.GetConvention(dictionaryType);

                            if (convention == null)
                            {
                                convention = new DictionaryTypeConvention(genericKeyType, genericValueType);
                            }

                            property.SetValue(objectToHydrate, convention.GenerateValue(property.GetCustomAttributes(true)), null);
                        }
                        else
                        {
                            var genericType = genericArguments[0];
                            var listType = typeof(List<>).MakeGenericType(genericType);

                            var convention = this.GetCustomConvention(baseType, previousType, propertyNamePrefix, property) ?? this.GetConvention(listType);

                            if (convention == null)
                            {
                                convention = new ListTypeConvention(genericType);
                            }

                            property.SetValue(objectToHydrate, convention.GenerateValue(property.GetCustomAttributes(true)), null);
                        }
                    }
                }
                else
                {
                    var propertyInstance = new object();
                    if (propertyType.HasParameterlessConstructor())
                    {
                        propertyInstance = Activator.CreateInstance(propertyType);
                        property.SetValue(objectToHydrate, propertyInstance, null);
                    }

                    var convention = this.GetCustomConvention(baseType, previousType, propertyNamePrefix, property) ?? this.GetConvention(propertyType);

                    if (convention != null)
                    {
                        property.SetValue(objectToHydrate, convention.GenerateValue(property.GetCustomAttributes(true)), null);
                    }
                    else
                    {
                        this.HydrateProperties(propertyInstance, property.PropertyType.GetProperties().Where(x => x.CanWrite), string.IsNullOrEmpty(propertyNamePrefix) ? propertyName : string.Format("{0}.{1}", propertyNamePrefix, propertyName), baseType, propertyType);
                    }
                }
            }
        }
    }
}
